Skip to content

Fix iPhone web client layout, audio, and tap latency#247

Open
serrebidev wants to merge 2 commits into
XGDevGroup:mainfrom
serrebidev:fix/iphone-mobile-menu-visibility
Open

Fix iPhone web client layout, audio, and tap latency#247
serrebidev wants to merge 2 commits into
XGDevGroup:mainfrom
serrebidev:fix/iphone-mobile-menu-visibility

Conversation

@serrebidev
Copy link
Copy Markdown

@serrebidev serrebidev commented May 4, 2026

Summary

Fix the iPhone web client issues seen while playing Mile by Mile: hidden panels leaking through the login view, awkward mobile panel order, delayed sound effects, and slow touch activation of menu/card items.

  • Ensure [hidden] always wins over author display rules so hidden UI actually stays hidden.
  • Split the web client into History, Game, and Chat panels with mobile order History -> Game -> Chat and desktop layout preserved.
  • Add iOS tap affordances for scrollable menu/action buttons.
  • Send touch menu selections on pointerup before playing activation audio, so card/item taps reach the server immediately.
  • Cache and preload Web Audio buffers for common UI, card, and Mile by Mile effects instead of creating a fresh Audio() element for every effect on demand.
  • Drop effects that would start too late, preventing stale sounds from playing many seconds after the action.
  • Preload per-menu item sounds from incoming menu packets and bump the web client version to 2026.05.07.1 for cache invalidation.

Root Cause

On iPhone Safari/WebKit, sound effects were being loaded at the moment of playback via new media elements. Slow media startup could then delay or back up effect playback. Touch activation also selected the item and ran sound work before sending the menu packet, which made taps feel delayed when audio loading stalled.

Validation

  • Verified on the live iPhone web client with the active table: sound effects no longer lag by ~20 seconds and Mile by Mile item taps activate promptly.
  • Ran node --check clients/web/audio.js.
  • Ran node --check clients/web/ui/menus.js.
  • Ran node --check clients/web/app.js.
  • Ran git diff --check.

serrebidev and others added 2 commits May 3, 2026 22:15
…ap defenses

iPhone players reported being unable to see history or the menu, and
when the menu was visible they could not tap cards to draw or play
them. Three issues compounded:

1. `.grid { display: grid }` in author CSS was overriding the UA
   stylesheet's `[hidden] { display: none }`, so `gameShell.hidden = true`
   did not actually hide the element. The login dialog backdrop happened
   to mask this pre-login, but the cascade is wrong. Add
   `[hidden] { display: none !important }` so the hidden attribute always
   wins.

2. The History+Chat panel was bundled together with chat at the bottom of
   it, sitting in column 2 on desktop and below the Game panel on mobile.
   Players asked for History at the top, Game in the middle, Chat at the
   bottom on phones. Split into three panels (history-panel / game-panel /
   chat-panel) and use `grid-template-areas` so:

   - Desktop keeps the same look: Game (left, full height), History
     (top-right), Chat (bottom-right).
   - Mobile: history → game → chat in a single column, all the way
     down.

3. Tap reliability on iPhone: buttons inside scrollable lists could feel
   unresponsive because of iOS Safari's touch-action heuristics and the
   missing `cursor: pointer`. Add `cursor: pointer`,
   `touch-action: manipulation`, `-webkit-tap-highlight-color`, and an
   `:active` background on `.menu-item-touch` and `.actions-item-btn` for
   visible tap feedback. Also add `-webkit-overflow-scrolling: touch` to
   `.menu-list` so momentum scrolling does not eat taps.

4. Compact volume controls on mobile: drop `flex-direction: column` from
   `.volume-controls` so the parent `.row` `flex-wrap: wrap` lets them
   flow horizontally; header drops from 162px to 50–106px.

Bump web client version to 2026.05.03.2 to invalidate cached app.js on
iOS Safari.

Verified in playwright iPhone emulation:
- Portrait 390x844: History (y=269), Game with full menu (y=359), Chat
  (y=719) all stack cleanly.
- Landscape 844x390: same order, all fits in 390px height.
- Desktop 1280x800: Game on left (full height), History top-right, Chat
  bottom-right.
- Click handler verified firing on .menu-item-touch buttons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@serrebidev serrebidev changed the title Fix iPhone web client: split panels into History → Game → Chat, add tap defenses Fix iPhone web client layout, audio, and tap latency May 8, 2026
@zarvox32
Copy link
Copy Markdown
Contributor

Reviewed and validated on the merge ref. node --check passes on all four changed JS files (audio.js, ui/menus.js, app.js, version.js). No automated web test infrastructure exists in the repo, so verification beyond syntax is limited to what you already did on a live iPhone. The claude-review CI failure here is infrastructure (OIDC token expiry on the action), not a code issue.

Most of this is solid work — the [hidden] !important fix is correct and well-commented, the pointer-event handling is careful (single-pointer tracking, 12px drag cancel, 750ms click-suppression window), the Web Audio buffer cache with LRU eviction is a real improvement over per-effect Audio() elements, and the stale-effect drop after 1.5s is a sensible game-feel choice. But there is one regression worth discussing before merge.

Desktop screen-reader/Tab order regression

The previous commit on these files (714fbf4 Mobile web client polish, same author) explicitly stated:

"CSS order flips History above Game on narrow viewports without changing DOM order, so Tab and screen-reader sequence stay desktop-correct (Game → History → Chat)."

That commit used order: 1/2 to flip visually on mobile while keeping DOM order Game → History on desktop. This PR reverses that decision: it physically moves the History <section> above Game in index.html, then uses grid-template-areas to position Game back on the left visually.

Net effect:

  • Desktop visual layout: unchanged (as the PR description claims).
  • Desktop tab / screen-reader / browse-mode reading order: changed from Game → History → Chat to History → Game → Chat.

For this project — CLAUDE.md says "minimize tab stops — every extra control is navigation overhead for screen reader users" — that means every state change a blind desktop player now lands in the History log before the interactive Game menu. The PR description says "desktop layout preserved" but doesn't acknowledge the DOM/SR-order change.

The clean fix

Keep the original DOM order (Game, History, Chat) and use grid-template-areas for both layouts — that's what the PR already does for desktop, just extend the same technique to mobile. Pure CSS change, no DOM reordering needed. The visual goal (History on top mobile, Game on left desktop) is achievable without affecting reading order.

If you really want History first in the DOM

Focus management can soften the initial-arrival case but doesn't substitute for DOM order:

<section id="game-panel" class="panel game-panel" tabindex="-1"></section>
gameShell.hidden = false;
document.getElementById("game-panel").focus();

This makes the screen reader announce "Game, heading level 2…" right when the shell appears, instead of starting at History. You could do the same on server state changes that return the turn to the user — focus the actions list so the new menu is announced immediately.

The limit: browse mode (NVDA, JAWS) and VoiceOver's virtual cursor walk DOM order, not focus. So when the user presses H for next heading, arrows through the page, or uses the heading rotor, they still hit History first. Focus management fixes the moment after login and after turn changes; it doesn't fix exploratory reading.

Auto-focusing on every server update is worse — it interrupts the user mid-sentence if they were reading the history log when their turn started.

So focus management is worth layering on regardless (the initial-arrival announcement is genuinely helpful), but for fixing the regression specifically, the DOM-order fix is one-line cheaper and has no edge cases.

Smaller notes (non-blocking)

Redundant preload list. The setTimeout(() => preloadEffects([…12 sounds]), 0) block at engine construction duplicates a literal subset of DEFAULT_PRELOAD_EFFECTS that unlock() also preloads. The dedup in queueEffectPreload prevents double-fetching so it's not buggy, but the two literal copies will drift. Either extract a CRITICAL_PRELOAD_EFFECTS constant or drop the setTimeout entirely — unlock runs on first user gesture, which is the earliest a buffered effect can actually play anyway.

cache: "force-cache" on fetch — relies on the version bump (2026.05.07.1) for invalidation. Sounds rarely change, so this is the right tradeoff.

Muting handles both paths. Element-based effects use effect.muted = muted; buffered effects set gain.gain.value = muted ? 0 : volume. Both correct. Buffered effects keep playing silently rather than stopping, which is fine for short SFX.

Happy to chat through the layout option you prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants